<!--
Tests that anchor gets updated as the state of the session changes.
-->
<html>
  <head>
    <link rel="stylesheet" type="text/css" href="../resources/webxr_e2e.css">
    <meta name="timeout" content="long">  <!-- this is a long-running test! -->
  </head>
  <body>
    <canvas id="webgl-canvas"></canvas>
    <script src="../../../../../../third_party/blink/web_tests/resources/testharness.js"></script>
    <script src="../resources/webxr_e2e.js"></script>
    <script>var shouldAutoCreateNonImmersiveSession = false;</script>
    <script src="../resources/webxr_boilerplate.js"></script>
    <script>
      setup({single_test: true});

      const TestState = Object.freeze({
        "Initial": 0,
        "HitTestSourceAvailable": 1,  // hitTestSource variable is guaranteed to be non-null in this state
        "AnchorRequested": 2,
        "AnchorAvailable": 3, // createdAnchor variable is guaranteed to be non-null in this state
        "AnchorPoseAvailable": 4, // createdAnchor and createdAnchorPose variables are guaranteed to be non-null in this state
        "AnchorPoseChanged": 5,
        "AnchorPaused": 6,
        "AnchorResumed": 7,
        "AnchorRemoved": 8,
        });

      let testState = TestState.Initial;
      let hitTestSource = null;
      let createdAnchor = null;
      let createdAnchorPose = null;

      let time_taken_in_ms = null;

      // Returns true if q1 and q2 represent the same (or almost the same) orientation.
      // This also means that if they only differ by sign, they would still be considered
      // equal (since they represent the same orientation, but take a different path).
      // |q1|, |q2| - DOMPoints representing quaternions.
      function orientation_almost_equal(q1, q2, epsilon) {
        const dot = q1.x * q2.x
                  + q1.y * q2.y
                  + q1.z * q2.z
                  + q1.w * q2.w;

        const angle = Math.acos(2 * dot * dot -1);

        return Math.abs(angle) < epsilon;
      };

      function position_almost_equal(p1, p2, epsilon) {
        const x_diff = p1.x - p2.x;
        const y_diff = p1.y - p2.y;
        const z_diff = p1.z - p2.z;

        const distance = Math.sqrt(x_diff * x_diff + y_diff * y_diff + z_diff * z_diff);

        return distance < epsilon;
      }

      function stepStartHitTesting() {
        const start_time = Date.now();

        const sessionInfo = sessionInfos[sessionTypes.AR];
        const referenceSpace = sessionInfo.currentRefSpace;

        sessionInfo.currentSession.requestHitTestSource({
          space: referenceSpace,
          offsetRay: new XRRay()
        }).then((hts) => {
          // Mark that the hit test source is available.
          hitTestSource = hts;
          testState = TestState.HitTestSourceAvailable;
        }).catch((err) => {
          assert_unreached("XRSession.requestHitTestSource() promise rejected.");
        });

        onARFrameCallback = (session, frame) => {

          switch(testState) {
            case TestState.Initial: {
              // In initial state, there is nothing to do (we're waiting for hit test source).
              return;
            }
            case TestState.HitTestSourceAvailable: {
              // Since we already have a hit test source, let's get its results and create an anchor.
              const results = frame.getHitTestResults(hitTestSource);
              if (results.length) {
                const result = results[0];
                result.createAnchor().then((anchor) => {
                  createdAnchor = anchor;
                  testState = TestState.AnchorAvailable;

                  hitTestSource.cancel();
                  hitTestSource = null;

                }).catch((err) => {
                  // Fail the test.
                  assert_unreached("XRHitTestResult.createAnchor() promise rejected.");
                });

                // Mark that we are waiting for anchor creation.
                testState = TestState.AnchorRequested;
              }
              return;
            }
            case TestState.AnchorAvailable: {
              assert_true(frame.trackedAnchors.has(createdAnchor), "Created anchor should be tracked!");

              const anchorPose = frame.getPose(createdAnchor.anchorSpace, referenceSpace);
              if (anchorPose) {
                testState = TestState.AnchorPoseAvailable;
                createdAnchorPose = anchorPose;
              }

              return;
            }
            case TestState.AnchorPoseAvailable: {
              assert_true(frame.trackedAnchors.has(createdAnchor), "Created anchor should be tracked!");

              const currentAnchorPose = frame.getPose(createdAnchor.anchorSpace, referenceSpace);
              if (currentAnchorPose) {
                // We'll only leave this state if anchor pose is significantly different than previous anchor pose.

                const previousAnchorPosition = createdAnchorPose.transform.position;
                const currentAnchorPosition = currentAnchorPose.transform.position;

                const previousAnchorOrientation = createdAnchorPose.transform.orientation;
                const currentAnchorOrientation = currentAnchorPose.transform.orientation;

                if (position_almost_equal(previousAnchorPosition, currentAnchorPosition, 0.05)
                    && orientation_almost_equal(previousAnchorOrientation, currentAnchorOrientation, 5 * (Math.PI / 180))) {
                  // Anchor pose is still the same, keep waiting.
                } else {
                  testState = TestState.AnchorPoseChanged;
                }
              }

              return;
            }
            case TestState.AnchorPoseChanged: {
              assert_true(frame.trackedAnchors.has(createdAnchor), "Created anchor should be tracked!");

              // Now let's wait for the anchor tracking to become paused:
              const currentAnchorPose = frame.getPose(createdAnchor.anchorSpace, referenceSpace);
              if(!currentAnchorPose) {
                testState = TestState.AnchorPaused;
              }

              return;
            }
            case TestState.AnchorPaused: {
              assert_true(frame.trackedAnchors.has(createdAnchor), "Created anchor should be tracked!");

              // Now wait for the tracking to be resumed:
              const currentAnchorPose = frame.getPose(createdAnchor.anchorSpace, referenceSpace);
              if(currentAnchorPose) {
                testState = TestState.AnchorResumed;
              }

              return;
            }
            case TestState.AnchorResumed: {
              assert_true(frame.trackedAnchors.has(createdAnchor), "Created anchor should be tracked!");

              createdAnchor.delete();

              assert_throws_dom('InvalidStateError', () => {
                  createdAnchor.anchorSpace;
              });

              testState = TestState.AnchorRemoved;

              return;
            }

            case TestState.AnchorRemoved:
              assert_true(!frame.trackedAnchors.has(createdAnchor), "Created anchor should not be tracked after it got removed!");

              time_taken_in_ms = Date.now() - start_time;
              done();
              return;
            default:
              return;
          }
        };
      }
    </script>
  </body>
</html>
